Add gene foundation: GeneDef, Genome, trait phenotype (Content)
CI / build-test (push) Successful in 1m17s

The organism-agnostic core of the gene system, built on the formula
engine. GeneDef is a def describing a gene's kind (numeric/discrete),
allele generation range/spread, mutation, variant distribution and its
effects on named traits as formulas. Genome is a managed, variable-
composition map (geneId -> Allele pair): generated from a gene set, bred
meiotically with per-gene mutation, expressed to a phenotype (numeric
mean / discrete lower-allele dominance); open composition allows hybrids.
Phenotype.Compute aggregates each gene's effect formulas into a trait
map (variable `value` = the gene's expressed phenotype, other gene ids
and an environment context resolve too), so systems read traits, never
genes.

Nothing here is species-specific. Def JSON now supports string-named
enums (JsonStringEnumConverter) so a gene's kind reads as "Discrete".
Covered by GeneticsTests (generation/expression/breeding/traits) and
GeneDefLoadTests (GeneDef through the real DefDatabase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 23:43:01 +03:00
co-authored by Claude Opus 4.8
parent 3438ed77f6
commit bc18db5df6
7 changed files with 642 additions and 0 deletions
@@ -0,0 +1,68 @@
using MrGameEng.Genetics;
using MrGameEng.Mods;
using Xunit;
namespace MrGameEng.Genetics.Tests;
/// <summary>
/// Verifies <see cref="GeneDef"/> loads through the real <see cref="DefDatabase"/> the way the game
/// loads <c>genes.json</c>: string-named <see cref="GeneKind"/>, parent inheritance and effect maps.
/// </summary>
public sealed class GeneDefLoadTests : IDisposable
{
private readonly string _root = Directory.CreateTempSubdirectory("mrge-gene-tests-").FullName;
public void Dispose() => Directory.Delete(_root, recursive: true);
private DefDatabase Load(string defsJson)
{
var modDir = Path.Combine(_root, "mod");
Directory.CreateDirectory(Path.Combine(modDir, "Defs"));
File.WriteAllText(Path.Combine(modDir, "Defs", "genes.json"), defsJson);
var database = new DefDatabase();
database.RegisterType<GeneDef>("Gene");
database.Load([new Mod(new ModInfo { Id = "mod" }, modDir)]);
return database;
}
[Fact]
public void Load_NumericGeneWithParentAndEffects_Resolves()
{
var database = Load(
"""
{ "type": "Gene", "defs": [
{ "defName": "BaseGene", "abstract": true, "spread": 0.1, "mutationChance": 0.05 },
{ "defName": "GeneVigor", "parent": "BaseGene", "default": 1.0, "min": 0.1, "max": 3.0,
"tags": ["growth"], "effects": { "vigor": "value" } }
]}
"""
);
var gene = database.Get<GeneDef>("GeneVigor");
Assert.Equal(GeneKind.Numeric, gene.Kind);
Assert.Equal(0.1f, gene.Spread); // inherited from parent
Assert.Equal(0.05f, gene.MutationChance); // inherited
Assert.Equal(3.0f, gene.Max);
Assert.Equal(["growth"], gene.Tags);
Assert.Equal("value", gene.Effects["vigor"]);
Assert.Single(gene.CompiledEffects); // formula compiles
}
[Fact]
public void Load_DiscreteKindByName_Parses()
{
var database = Load(
"""
{ "type": "Gene", "defs": [
{ "defName": "GeneMorph", "kind": "Discrete", "variants": 2,
"variantWeights": [0.8, 0.2], "effects": { "variant": "value" } }
]}
"""
);
var gene = database.Get<GeneDef>("GeneMorph");
Assert.Equal(GeneKind.Discrete, gene.Kind);
Assert.Equal(2, gene.Variants);
Assert.Equal([0.8f, 0.2f], gene.VariantWeights);
}
}