G3: migrate plants onto genes/traits (drop fixed PlantGenome)

Plants now carry a managed Genome and read formula-computed traits
instead of the hardcoded PlantGenome struct. Engine pointer -> ead2251
(GenomeTemplate + Breed mutationChance override).

- PlantOrganism { Genome; PlantPhenotype Traits } replaces PlantGenome.
  Traits (optima/tolerances, vigor, lifespan, dispersal, reproduce
  interval, self-pollination, mutation rate, morph variant) come from
  Phenotype.Compute over the gene effect formulas in genes.json.
- PlantSet builds a per-species GenomeTemplate by mapping each species'
  existing plants.json genome numbers onto the shared GeneDefs, and
  exposes the gene registry. Species values unchanged — only reinterpreted
  through genes now.
- Growth and lifecycle systems read PlantOrganism.Traits; breeding goes
  through Genome.Breed with the registry and a mutation chance averaged
  from the parents' evolvable mutationRate trait. Scatter generates from
  the species template.
- Save stores the genome as a geneId->Allele map; load rebuilds it
  (empty/legacy saves regenerate from the template — dev saves disposable).

Behaviour matches phases A-D, now fully data-driven through genes and
formulas. Full suite green; boot smoke test loads genes + plants cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 23:59:46 +03:00
co-authored by Claude Opus 4.8
parent 91a00b4305
commit 9c2c2d7fd0
10 changed files with 208 additions and 220 deletions
+1 -1
Submodule engine updated: bc18db5df6...ead22517ee
+4 -4
View File
@@ -1,5 +1,5 @@
using System.Text.Json; using System.Text.Json;
using LittleSim.Sim; using MrGameEng.Genetics;
namespace LittleSim.App; namespace LittleSim.App;
@@ -24,8 +24,8 @@ public sealed class PlantSave
/// <summary>Плодородность клетки.</summary> /// <summary>Плодородность клетки.</summary>
public float CellFertility { get; set; } public float CellFertility { get; set; }
/// <summary>Геном особи (аллели сериализуются как поля структуры).</summary> /// <summary>Геном особи: аллели по id генов (переменного состава).</summary>
public PlantGenome Genome { get; set; } public Dictionary<string, Allele> Genome { get; set; } = new();
} }
/// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary> /// <summary>Сериализуемое состояние одного жителя (sim-компоненты; спрайт пересобирается из дефа по <see cref="DefName"/>).</summary>
@@ -119,7 +119,7 @@ public sealed class SaveStore
{ {
WriteIndented = true, WriteIndented = true,
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
IncludeFields = true, // аллели генома (PlantGenome) — публичные поля структуры IncludeFields = true, // на случай публичных полей в сохраняемых sim-структурах
}; };
private readonly string _directory; private readonly string _directory;
+57 -1
View File
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
namespace LittleSim.Content; namespace LittleSim.Content;
@@ -23,6 +24,9 @@ public sealed class PlantSet
/// <summary>Стадии роста по порядку; последняя — терминальная.</summary> /// <summary>Стадии роста по порядку; последняя — терминальная.</summary>
public required Stage[] Stages { get; init; } public required Stage[] Stages { get; init; }
/// <summary>Набор генов вида: базовые значения для генерации особи (геном из общих <see cref="GeneDef"/>).</summary>
public required GenomeTemplate Template { get; init; }
/// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary> /// <summary>День, с которого растение достигает последней (зрелой) стадии.</summary>
public float MaturityDays => Stages[^1].EnterDay; public float MaturityDays => Stages[^1].EnterDay;
@@ -42,15 +46,28 @@ public sealed class PlantSet
private readonly Species[] _species; private readonly Species[] _species;
private readonly Dictionary<PlantDef, int> _index = new(); private readonly Dictionary<PlantDef, int> _index = new();
/// <summary>Общий реестр генов (по id) для скрещивания и вычисления признаков растений.</summary>
public IReadOnlyDictionary<string, GeneDef> GeneRegistry { get; }
/// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary> /// <summary>Строит таблицу из всех (не-abstract) <see cref="PlantDef"/> мода.</summary>
public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device) public PlantSet(GameContent content, ModAtlases atlases, GraphicsDevice device)
{ {
var genes = content
.Defs.All<GeneDef>()
.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
GeneRegistry = genes;
var defs = content.Defs.All<PlantDef>().ToArray(); var defs = content.Defs.All<PlantDef>().ToArray();
_species = new Species[defs.Length]; _species = new Species[defs.Length];
for (var i = 0; i < defs.Length; i++) for (var i = 0; i < defs.Length; i++)
{ {
var def = defs[i]; var def = defs[i];
_species[i] = new Species { Def = def, Stages = BuildStages(def, atlases, device) }; _species[i] = new Species
{
Def = def,
Stages = BuildStages(def, atlases, device),
Template = BuildTemplate(def.Genome, genes),
};
_index[def] = i; _index[def] = i;
} }
} }
@@ -64,6 +81,45 @@ public sealed class PlantSet
/// <summary>Индекс вида по дефу.</summary> /// <summary>Индекс вида по дефу.</summary>
public int IndexOf(PlantDef def) => _index[def]; public int IndexOf(PlantDef def) => _index[def];
// Переводит базовый геном вида (числа из plants.json) в набор генов: каждое число — центр
// аллелей соответствующего общего GeneDef; морфа — дискретный ген с долей рецессива из вида.
private static GenomeTemplate BuildTemplate(
GenomeDef g,
IReadOnlyDictionary<string, GeneDef> genes
)
{
GeneDef Gene(string id) =>
genes.TryGetValue(id, out var def)
? def
: throw new KeyNotFoundException(
$"Gene def '{id}' not found — is Mods/Core/Defs/genes.json loaded?"
);
GenomeTemplate.Entry Numeric(string id, float baseValue) =>
new(Gene(id), baseValue, g.Spread);
return new GenomeTemplate([
Numeric("GeneOptimalLight", g.OptimalLight),
Numeric("GeneLightTolerance", g.LightTolerance),
Numeric("GeneOptimalTemperature", g.OptimalTemperature),
Numeric("GeneTemperatureTolerance", g.TemperatureTolerance),
Numeric("GeneOptimalFertility", g.OptimalFertility),
Numeric("GeneFertilityTolerance", g.FertilityTolerance),
Numeric("GeneVigor", g.Vigor),
Numeric("GeneLifespan", g.Lifespan),
Numeric("GeneDispersalRange", g.DispersalRange),
Numeric("GeneReproduceInterval", g.ReproduceInterval),
Numeric("GeneSelfPollination", g.SelfPollination),
Numeric("GeneMutationRate", g.MutationRate),
new GenomeTemplate.Entry(
Gene("GeneMorph"),
0f,
0f,
[1f - g.VariantChance, g.VariantChance]
),
]);
}
private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device) private static Stage[] BuildStages(PlantDef def, ModAtlases atlases, GraphicsDevice device)
{ {
// Без явных стадий растение существует как одна терминальная стадия из базовой текстуры. // Без явных стадий растение существует как одна терминальная стадия из базовой текстуры.
+1 -1
View File
@@ -46,7 +46,7 @@ internal static class ScatterSpawner
index, index,
new Vector2(px, py), new Vector2(px, py),
age, age,
PlantGenome.FromDef(def.Genome, random), plants[index].Template.Generate(random),
terrain.Fertility, terrain.Fertility,
cellSize cellSize
); );
+15 -5
View File
@@ -353,9 +353,14 @@ public sealed class WorldScene : Scene
// Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном. // Снимок всей популяции растений: вид (деф), позиция, возраст, стадия, почва и геном.
Store Store
.Query<PlantGrowth, PlantGenome, Transform2D>() .Query<PlantGrowth, PlantOrganism, Transform2D>()
.ForEachEntity( .ForEachEntity(
(ref PlantGrowth grow, ref PlantGenome gene, ref Transform2D transform, Entity _) => (
ref PlantGrowth grow,
ref PlantOrganism org,
ref Transform2D transform,
Entity _
) =>
{ {
save.Plants.Add( save.Plants.Add(
new PlantSave new PlantSave
@@ -366,7 +371,7 @@ public sealed class WorldScene : Scene
AgeDays = grow.AgeDays, AgeDays = grow.AgeDays,
Stage = grow.Stage, Stage = grow.Stage,
CellFertility = grow.CellFertility, CellFertility = grow.CellFertility,
Genome = gene, Genome = org.Genome.ToDictionary(),
} }
); );
} }
@@ -379,6 +384,7 @@ public sealed class WorldScene : Scene
private void RestorePlants(GameContent content) private void RestorePlants(GameContent content)
{ {
var fallback = new Random(_config.Seed); // для старых/битых сейвов без генома
foreach (var plant in _save!.Plants) foreach (var plant in _save!.Plants)
{ {
if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def)) if (!content.Defs.TryGet<PlantDef>(plant.Species, out var def))
@@ -386,13 +392,17 @@ public sealed class WorldScene : Scene
continue; // вид пропал (мод убрали) — пропускаем растение continue; // вид пропал (мод убрали) — пропускаем растение
} }
var index = _plants.IndexOf(def);
var genome = plant.Genome is { Count: > 0 }
? new Genome(plant.Genome)
: _plants[index].Template.Generate(fallback);
PlantFactory.Create( PlantFactory.Create(
Store, Store,
_plants, _plants,
_plants.IndexOf(def), index,
new Vector2(plant.X, plant.Y), new Vector2(plant.X, plant.Y),
plant.AgeDays, plant.AgeDays,
plant.Genome, genome,
plant.CellFertility, plant.CellFertility,
CellSize CellSize
); );
+8 -6
View File
@@ -2,14 +2,15 @@ using Friflo.Engine.ECS;
using LittleSim.Content; using LittleSim.Content;
using LittleSim.Scenes; using LittleSim.Scenes;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
namespace LittleSim.Sim; namespace LittleSim.Sim;
/// <summary> /// <summary>
/// Единая точка создания сущности-растения: спрайт стадии по возрасту, тинт по морфе генома и /// Единая точка создания сущности-растения: спрайт стадии по возрасту, признаки из генома формулами
/// компоненты роста/генома. Переиспользуется начальным скаттером, размножением и загрузкой сейва — /// генов, тинт по морфе и компоненты роста/организма. Переиспользуется начальным скаттером,
/// чтобы все растения собирались одинаково. /// размножением и загрузкой сейва — чтобы все растения собирались одинаково.
/// </summary> /// </summary>
public static class PlantFactory public static class PlantFactory
{ {
@@ -23,7 +24,7 @@ public static class PlantFactory
int species, int species,
Vector2 position, Vector2 position,
float ageDays, float ageDays,
in PlantGenome genome, Genome genome,
float cellFertility, float cellFertility,
int cellSize int cellSize
) )
@@ -31,10 +32,11 @@ public static class PlantFactory
var sp = plants[species]; var sp = plants[species];
var stage = sp.StageAt(ageDays); var stage = sp.StageAt(ageDays);
var resolved = sp.Stages[stage]; var resolved = sp.Stages[stage];
var traits = PlantPhenotype.FromTraits(Phenotype.Compute(genome, plants.GeneRegistry));
var sprite = new Sprite(resolved.Region, GameLayers.Beings); var sprite = new Sprite(resolved.Region, GameLayers.Beings);
sprite.CenterOrigin(); sprite.CenterOrigin();
sprite.Color = genome.IsVariant ? VariantTint : Color.White; sprite.Color = traits.IsVariant ? VariantTint : Color.White;
return store.CreateEntity( return store.CreateEntity(
new Transform2D( new Transform2D(
@@ -49,7 +51,7 @@ public static class PlantFactory
AgeDays = ageDays, AgeDays = ageDays,
CellFertility = cellFertility, CellFertility = cellFertility,
}, },
genome new PlantOrganism { Genome = genome, Traits = traits }
); );
} }
} }
-171
View File
@@ -1,171 +0,0 @@
using Friflo.Engine.ECS;
using LittleSim.Content;
namespace LittleSim.Sim;
/// <summary>
/// Диплоидный геном растения: по паре аллелей на ген. Числовые гены экспрессируются гибридно —
/// фенотип есть среднее двух аллелей; дискретный ген морфы — по Менделю (рецессив виден только в
/// гомозиготе). Аллели наследуются (правятся в инспекторе); фенотип-свойства читают системы роста и
/// жизненного цикла. <see cref="Breed"/> моделирует мейоз и мутацию.
/// </summary>
public struct PlantGenome : IComponent
{
// --- Числовые гены (аллель A/B), фенотип = среднее ------------------------------------------
public float OptimalLightA;
public float OptimalLightB;
public float LightToleranceA;
public float LightToleranceB;
public float OptimalTemperatureA;
public float OptimalTemperatureB;
public float TemperatureToleranceA;
public float TemperatureToleranceB;
public float OptimalFertilityA;
public float OptimalFertilityB;
public float FertilityToleranceA;
public float FertilityToleranceB;
public float VigorA;
public float VigorB;
public float LifespanA;
public float LifespanB;
public float DispersalRangeA;
public float DispersalRangeB;
public float ReproduceIntervalA;
public float ReproduceIntervalB;
public float SelfPollinationA;
public float SelfPollinationB;
public float MutationRateA;
public float MutationRateB;
// --- Дискретный ген морфы (0 — доминантный, 1 — рецессивный) --------------------------------
public int MorphA;
public int MorphB;
/// <summary>Фенотип: оптимальная освещённость.</summary>
public readonly float OptimalLight => (OptimalLightA + OptimalLightB) * 0.5f;
/// <summary>Фенотип: толерантность по свету.</summary>
public readonly float LightTolerance => (LightToleranceA + LightToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная температура.</summary>
public readonly float OptimalTemperature => (OptimalTemperatureA + OptimalTemperatureB) * 0.5f;
/// <summary>Фенотип: толерантность по температуре.</summary>
public readonly float TemperatureTolerance =>
(TemperatureToleranceA + TemperatureToleranceB) * 0.5f;
/// <summary>Фенотип: оптимальная плодородность.</summary>
public readonly float OptimalFertility => (OptimalFertilityA + OptimalFertilityB) * 0.5f;
/// <summary>Фенотип: толерантность по плодородности.</summary>
public readonly float FertilityTolerance => (FertilityToleranceA + FertilityToleranceB) * 0.5f;
/// <summary>Фенотип: бодрость роста.</summary>
public readonly float Vigor => (VigorA + VigorB) * 0.5f;
/// <summary>Фенотип: продолжительность жизни (дней).</summary>
public readonly float Lifespan => (LifespanA + LifespanB) * 0.5f;
/// <summary>Фенотип: дальность расселения (клеток).</summary>
public readonly float DispersalRange => (DispersalRangeA + DispersalRangeB) * 0.5f;
/// <summary>Фенотип: интервал размножения (дней).</summary>
public readonly float ReproduceInterval => (ReproduceIntervalA + ReproduceIntervalB) * 0.5f;
/// <summary>Фенотип: способность к самоопылению (0..1).</summary>
public readonly float SelfPollination => (SelfPollinationA + SelfPollinationB) * 0.5f;
/// <summary>Фенотип: темп мутаций (0..1).</summary>
public readonly float MutationRate => (MutationRateA + MutationRateB) * 0.5f;
/// <summary>Фенотип морфы: рецессивный вариант проявляется только в гомозиготе (1,1).</summary>
public readonly bool IsVariant => MorphA == 1 && MorphB == 1;
/// <summary>
/// Генерирует особь из базового генома вида: числовые аллели — база ± относительный разброс
/// (<see cref="GenomeDef.Spread"/>); аллель морфы = рецессивный (1) с вероятностью
/// <see cref="GenomeDef.VariantChance"/>. Сидируется <paramref name="random"/> — детерминизм.
/// </summary>
public static PlantGenome FromDef(GenomeDef def, Random random)
{
float Allele(float baseValue) =>
baseValue + (random.NextSingle() * 2f - 1f) * def.Spread * baseValue;
int MorphAllele() => random.NextSingle() < def.VariantChance ? 1 : 0;
return new PlantGenome
{
OptimalLightA = Allele(def.OptimalLight),
OptimalLightB = Allele(def.OptimalLight),
LightToleranceA = Allele(def.LightTolerance),
LightToleranceB = Allele(def.LightTolerance),
OptimalTemperatureA = Allele(def.OptimalTemperature),
OptimalTemperatureB = Allele(def.OptimalTemperature),
TemperatureToleranceA = Allele(def.TemperatureTolerance),
TemperatureToleranceB = Allele(def.TemperatureTolerance),
OptimalFertilityA = Allele(def.OptimalFertility),
OptimalFertilityB = Allele(def.OptimalFertility),
FertilityToleranceA = Allele(def.FertilityTolerance),
FertilityToleranceB = Allele(def.FertilityTolerance),
VigorA = Allele(def.Vigor),
VigorB = Allele(def.Vigor),
LifespanA = Allele(def.Lifespan),
LifespanB = Allele(def.Lifespan),
DispersalRangeA = Allele(def.DispersalRange),
DispersalRangeB = Allele(def.DispersalRange),
ReproduceIntervalA = Allele(def.ReproduceInterval),
ReproduceIntervalB = Allele(def.ReproduceInterval),
SelfPollinationA = Allele(def.SelfPollination),
SelfPollinationB = Allele(def.SelfPollination),
MutationRateA = Allele(def.MutationRate),
MutationRateB = Allele(def.MutationRate),
MorphA = MorphAllele(),
MorphB = MorphAllele(),
};
}
/// <summary>
/// Потомок двух родителей (мейоз): аллель A — случайный из родителя <paramref name="a"/>, аллель
/// B — из <paramref name="b"/>; затем мутация (с вероятностью среднего темпа мутаций родителей —
/// сдвиг числового аллеля на малую долю / переключение морфы). Самоопыление = <c>Breed(self, self)</c>.
/// </summary>
public static PlantGenome Breed(in PlantGenome a, in PlantGenome b, Random rng)
{
var rate = (a.MutationRate + b.MutationRate) * 0.5f;
float Mut(float v) =>
rng.NextSingle() < rate ? v + (rng.NextSingle() * 2f - 1f) * 0.12f * v : v;
float FromA(float x, float y) => Mut(rng.NextSingle() < 0.5f ? x : y);
int MutMorph(int m) => rng.NextSingle() < rate ? 1 - m : m;
return new PlantGenome
{
OptimalLightA = FromA(a.OptimalLightA, a.OptimalLightB),
OptimalLightB = FromA(b.OptimalLightA, b.OptimalLightB),
LightToleranceA = FromA(a.LightToleranceA, a.LightToleranceB),
LightToleranceB = FromA(b.LightToleranceA, b.LightToleranceB),
OptimalTemperatureA = FromA(a.OptimalTemperatureA, a.OptimalTemperatureB),
OptimalTemperatureB = FromA(b.OptimalTemperatureA, b.OptimalTemperatureB),
TemperatureToleranceA = FromA(a.TemperatureToleranceA, a.TemperatureToleranceB),
TemperatureToleranceB = FromA(b.TemperatureToleranceA, b.TemperatureToleranceB),
OptimalFertilityA = FromA(a.OptimalFertilityA, a.OptimalFertilityB),
OptimalFertilityB = FromA(b.OptimalFertilityA, b.OptimalFertilityB),
FertilityToleranceA = FromA(a.FertilityToleranceA, a.FertilityToleranceB),
FertilityToleranceB = FromA(b.FertilityToleranceA, b.FertilityToleranceB),
VigorA = FromA(a.VigorA, a.VigorB),
VigorB = FromA(b.VigorA, b.VigorB),
LifespanA = FromA(a.LifespanA, a.LifespanB),
LifespanB = FromA(b.LifespanA, b.LifespanB),
DispersalRangeA = FromA(a.DispersalRangeA, a.DispersalRangeB),
DispersalRangeB = FromA(b.DispersalRangeA, b.DispersalRangeB),
ReproduceIntervalA = FromA(a.ReproduceIntervalA, a.ReproduceIntervalB),
ReproduceIntervalB = FromA(b.ReproduceIntervalA, b.ReproduceIntervalB),
SelfPollinationA = FromA(a.SelfPollinationA, a.SelfPollinationB),
SelfPollinationB = FromA(b.SelfPollinationA, b.SelfPollinationB),
MutationRateA = FromA(a.MutationRateA, a.MutationRateB),
MutationRateB = FromA(b.MutationRateA, b.MutationRateB),
MorphA = MutMorph(rng.NextSingle() < 0.5f ? a.MorphA : a.MorphB),
MorphB = MutMorph(rng.NextSingle() < 0.5f ? b.MorphA : b.MorphB),
};
}
}
+14 -13
View File
@@ -11,9 +11,10 @@ namespace LittleSim.Sim;
/// <summary> /// <summary>
/// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов /// Двигает рост растений по игровому календарю: прирост возраста = пригодность среды для генов
/// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) · /// растения. Скорость = <c>бодрость · пригодность(свет) · пригодность(температура) ·
/// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из генов /// пригодность(плодородность)</c>, каждая пригодность — гауссов «колокол» вокруг оптимума из
/// (см. <see cref="PlantGenome"/>). Свет/температура в фазе B глобальные (день/ночь и сезон); стадии /// признаков (<see cref="PlantPhenotype"/>), вычисленных формулами генов. Свет локальный (лайтмап),
/// и спрайт переключаются по накопленному возрасту как прежде. Терминальную стадию пропускает. /// температура глобальная (сезон/день-ночь); стадии и спрайт переключаются по накопленному
/// возрасту как прежде. Терминальную стадию пропускает.
/// </summary> /// </summary>
public sealed class PlantGrowthSystem( public sealed class PlantGrowthSystem(
PlantSet plants, PlantSet plants,
@@ -21,7 +22,7 @@ public sealed class PlantGrowthSystem(
Climate climate, Climate climate,
Lighting lighting, Lighting lighting,
int cellSize int cellSize
) : QuerySystem<PlantGrowth, PlantGenome, Sprite, Transform2D> ) : QuerySystem<PlantGrowth, PlantOrganism, Sprite, Transform2D>
{ {
protected override void OnUpdate() protected override void OnUpdate()
{ {
@@ -34,10 +35,10 @@ public sealed class PlantGrowthSystem(
// Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее). // Температура глобальная; свет — локальный (лайтмап: подлесок под кронами темнее).
var temperature = climate.Temperature; var temperature = climate.Temperature;
foreach (var (growths, genomes, sprites, transforms, _) in Query.Chunks) foreach (var (growths, organisms, sprites, transforms, _) in Query.Chunks)
{ {
var g = growths.Span; var g = growths.Span;
var dna = genomes.Span; var orgs = organisms.Span;
var s = sprites.Span; var s = sprites.Span;
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < g.Length; i++) for (var i = 0; i < g.Length; i++)
@@ -50,20 +51,20 @@ public sealed class PlantGrowthSystem(
continue; continue;
} }
ref var gene = ref dna[i]; ref var traits = ref orgs[i].Traits;
var light = lighting.SampleAt(t[i].Position); var light = lighting.SampleAt(t[i].Position);
var rate = var rate =
gene.Vigor traits.Vigor
* Suitability.Gaussian(light, gene.OptimalLight, gene.LightTolerance) * Suitability.Gaussian(light, traits.OptimalLight, traits.LightTolerance)
* Suitability.Gaussian( * Suitability.Gaussian(
temperature, temperature,
gene.OptimalTemperature, traits.OptimalTemperature,
gene.TemperatureTolerance traits.TemperatureTolerance
) )
* Suitability.Gaussian( * Suitability.Gaussian(
grow.CellFertility, grow.CellFertility,
gene.OptimalFertility, traits.OptimalFertility,
gene.FertilityTolerance traits.FertilityTolerance
); );
grow.AgeDays += deltaDays * rate; grow.AgeDays += deltaDays * rate;
+42 -18
View File
@@ -3,6 +3,7 @@ using Friflo.Engine.ECS.Systems;
using LittleSim.Content; using LittleSim.Content;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Genetics;
using MrGameEng.Graphics; using MrGameEng.Graphics;
namespace LittleSim.Sim; namespace LittleSim.Sim;
@@ -31,7 +32,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
private readonly bool[] _cellLand; private readonly bool[] _cellLand;
private readonly int _densityCap; private readonly int _densityCap;
private readonly Random _rng; private readonly Random _rng;
private readonly ArchetypeQuery<PlantGrowth, PlantGenome, Transform2D> _query; private readonly ArchetypeQuery<PlantGrowth, PlantOrganism, Transform2D> _query;
private readonly int[] _count; private readonly int[] _count;
private readonly CellRep[] _rep; private readonly CellRep[] _rep;
@@ -65,7 +66,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
_cellLand = cellLand; _cellLand = cellLand;
_densityCap = densityCap; _densityCap = densityCap;
_rng = new Random(seed); _rng = new Random(seed);
_query = store.Query<PlantGrowth, PlantGenome, Transform2D>(); _query = store.Query<PlantGrowth, PlantOrganism, Transform2D>();
_count = new int[width * height]; _count = new int[width * height];
_rep = new CellRep[width * height]; _rep = new CellRep[width * height];
} }
@@ -99,16 +100,16 @@ public sealed class PlantLifecycleSystem : BaseSystem
_candidates.Clear(); _candidates.Clear();
_births.Clear(); _births.Clear();
foreach (var (growths, genomes, transforms, entities) in _query.Chunks) foreach (var (growths, organisms, transforms, entities) in _query.Chunks)
{ {
var g = growths.Span; var g = growths.Span;
var dna = genomes.Span; var orgs = organisms.Span;
var t = transforms.Span; var t = transforms.Span;
for (var i = 0; i < g.Length; i++) for (var i = 0; i < g.Length; i++)
{ {
ref var grow = ref g[i]; ref var grow = ref g[i];
ref var gene = ref dna[i]; ref var org = ref orgs[i];
if (grow.AgeDays > gene.Lifespan) if (grow.AgeDays > org.Traits.Lifespan)
{ {
_deaths.Add(entities.EntityAt(i)); // умер от старости _deaths.Add(entities.EntityAt(i)); // умер от старости
continue; continue;
@@ -125,7 +126,8 @@ public sealed class PlantLifecycleSystem : BaseSystem
{ {
Species = grow.Species, Species = grow.Species,
Mature = mature, Mature = mature,
Genome = gene, Genome = org.Genome,
MutationRate = org.Traits.MutationRate,
}; };
if (mature) if (mature)
{ {
@@ -134,7 +136,8 @@ public sealed class PlantLifecycleSystem : BaseSystem
{ {
Cell = cell, Cell = cell,
Species = grow.Species, Species = grow.Species,
Genome = gene, Genome = org.Genome,
Traits = org.Traits,
} }
); );
} }
@@ -147,24 +150,29 @@ public sealed class PlantLifecycleSystem : BaseSystem
{ {
foreach (var cand in _candidates) foreach (var cand in _candidates)
{ {
var interval = MathF.Max(0.5f, cand.Genome.ReproduceInterval); var interval = MathF.Max(0.5f, cand.Traits.ReproduceInterval);
if (_rng.NextSingle() >= elapsed / interval) if (_rng.NextSingle() >= elapsed / interval)
{ {
continue; // в этот тик не сеет continue; // в этот тик не сеет
} }
var range = Math.Clamp((int)MathF.Round(cand.Genome.DispersalRange), 1, 6); var range = Math.Clamp((int)MathF.Round(cand.Traits.DispersalRange), 1, 6);
var cx = cand.Cell % _width; var cx = cand.Cell % _width;
var cy = cand.Cell / _width; var cy = cand.Cell / _width;
if (!TryFindPartner(cx, cy, range, cand.Species, out var partner)) if (!TryFindPartner(cx, cy, range, cand.Species, out var partner))
{ {
if (_rng.NextSingle() >= cand.Genome.SelfPollination) if (_rng.NextSingle() >= cand.Traits.SelfPollination)
{ {
continue; // нет партнёра и самоопыление не удалось continue; // нет партнёра и самоопыление не удалось
} }
partner = cand.Genome; // самоопыление // самоопыление — партнёр это сам кандидат
partner = new Partner
{
Genome = cand.Genome,
MutationRate = cand.Traits.MutationRate,
};
} }
if (!TryPickTarget(cx, cy, range, out var tcell)) if (!TryPickTarget(cx, cy, range, out var tcell))
@@ -172,7 +180,15 @@ public sealed class PlantLifecycleSystem : BaseSystem
continue; // некуда сеять (нет суши/свободного места рядом) continue; // некуда сеять (нет суши/свободного места рядом)
} }
var child = PlantGenome.Breed(cand.Genome, partner, _rng); // Темп мутаций потомка — средний эволюционирующий признак родителей (ген управляет мутацией).
var mutationChance = (cand.Traits.MutationRate + partner.MutationRate) * 0.5f;
var child = Genome.Breed(
cand.Genome,
partner.Genome,
_plants.GeneRegistry,
_rng,
mutationChance
);
var tx = tcell % _width; var tx = tcell % _width;
var ty = tcell / _width; var ty = tcell / _width;
var position = new Vector2( var position = new Vector2(
@@ -192,7 +208,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
} }
} }
private bool TryFindPartner(int cx, int cy, int range, int species, out PlantGenome partner) private bool TryFindPartner(int cx, int cy, int range, int species, out Partner partner)
{ {
for (var dy = -range; dy <= range; dy++) for (var dy = -range; dy <= range; dy++)
{ {
@@ -213,7 +229,7 @@ public sealed class PlantLifecycleSystem : BaseSystem
ref var rep = ref _rep[ny * _width + nx]; ref var rep = ref _rep[ny * _width + nx];
if (rep.Species == species && rep.Mature) if (rep.Species == species && rep.Mature)
{ {
partner = rep.Genome; partner = new Partner { Genome = rep.Genome, MutationRate = rep.MutationRate };
return true; return true;
} }
} }
@@ -272,21 +288,29 @@ public sealed class PlantLifecycleSystem : BaseSystem
{ {
public int Species; public int Species;
public bool Mature; public bool Mature;
public PlantGenome Genome; public Genome Genome;
public float MutationRate;
} }
private struct Candidate private struct Candidate
{ {
public int Cell; public int Cell;
public int Species; public int Species;
public PlantGenome Genome; public Genome Genome;
public PlantPhenotype Traits;
}
private struct Partner
{
public Genome Genome;
public float MutationRate;
} }
private struct Birth private struct Birth
{ {
public int Species; public int Species;
public Vector2 Position; public Vector2 Position;
public PlantGenome Genome; public Genome Genome;
public float Fertility; public float Fertility;
} }
} }
+66
View File
@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Friflo.Engine.ECS;
using MrGameEng.Genetics;
namespace LittleSim.Sim;
/// <summary>
/// Растительные признаки (фенотип), вычисленные из генома формулами генов (см. <c>genes.json</c>):
/// готовые значения, которые читают системы роста и жизненного цикла, не касаясь генов напрямую.
/// Имена полей соответствуют именам признаков в эффектах генов.
/// </summary>
public struct PlantPhenotype
{
public float OptimalLight;
public float LightTolerance;
public float OptimalTemperature;
public float TemperatureTolerance;
public float OptimalFertility;
public float FertilityTolerance;
public float Vigor;
public float Lifespan;
public float DispersalRange;
public float ReproduceInterval;
public float SelfPollination;
public float MutationRate;
/// <summary>Морфа: рецессивный вариант (выраженное значение гена морфы ≈ 1) — другой тинт спрайта.</summary>
public bool IsVariant;
/// <summary>Собирает фенотип из карты признаков, посчитанной <see cref="Phenotype.Compute"/>.</summary>
public static PlantPhenotype FromTraits(IReadOnlyDictionary<string, float> traits)
{
float T(string name) => traits.GetValueOrDefault(name);
return new PlantPhenotype
{
OptimalLight = T("optimalLight"),
LightTolerance = T("lightTolerance"),
OptimalTemperature = T("optimalTemperature"),
TemperatureTolerance = T("temperatureTolerance"),
OptimalFertility = T("optimalFertility"),
FertilityTolerance = T("fertilityTolerance"),
Vigor = T("vigor"),
Lifespan = T("lifespan"),
DispersalRange = T("dispersalRange"),
ReproduceInterval = T("reproduceInterval"),
SelfPollination = T("selfPollination"),
MutationRate = T("mutationRate"),
IsVariant = T("variant") >= 0.5f,
};
}
}
/// <summary>
/// Компонент-организм растения: его управляемый <see cref="Genome"/> (для размножения, сейва и
/// пересчёта) и выраженный фенотип <see cref="Traits"/>, по которому работают системы. Заменяет
/// прежний фиксированный <c>PlantGenome</c> — теперь геном переменного состава и признаки из формул
/// генов (фаза G3). Геном — ссылочный объект, поэтому компонент несёт ссылку, а не копию.
/// </summary>
public struct PlantOrganism : IComponent
{
/// <summary>Геном особи (аллели по генам); общий источник истины для признаков и размножения.</summary>
public Genome Genome;
/// <summary>Выраженные признаки — то, что читают системы роста/жизненного цикла.</summary>
public PlantPhenotype Traits;
}