diff --git a/LittleSim.sln b/LittleSim.sln index 90a0b1e..15d0529 100644 --- a/LittleSim.sln +++ b/LittleSim.sln @@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host", "engine\src\MrGameEng.Host\MrGameEng.Host.csproj", "{6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Host.Tests", "engine\tests\MrGameEng.Host.Tests\MrGameEng.Host.Tests.csproj", "{BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -231,6 +235,30 @@ Global {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x64.Build.0 = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = Release|Any CPU {E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x64.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Debug|x86.Build.0 = Debug|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|Any CPU.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x64.Build.0 = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.ActiveCfg = Release|Any CPU + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6}.Release|x86.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x64.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Debug|x86.Build.0 = Debug|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|Any CPU.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x64.Build.0 = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.ActiveCfg = Release|Any CPU + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -253,5 +281,7 @@ Global {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {E0E37D87-4F62-41E9-9CD1-3E7432301508} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} + {6EFAFBB4-F11A-42B9-A8AB-B98E735431F6} = {F4A69A46-AF86-AB4E-7D23-4CCCB7B9A519} + {BE18D2E0-B8AD-4632-AA0F-F9C80BE5D83A} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} EndGlobalSection EndGlobal diff --git a/docs/гены.md b/docs/гены.md new file mode 100644 index 0000000..60bed8d --- /dev/null +++ b/docs/гены.md @@ -0,0 +1,104 @@ +# Гены как фундамент + +Гены — **основополагающая** система LittleSim, общая для растений, животных и людей, а не +структура под один вид. Геном — переменного состава, поэтому возможны произвольные **гибриды** +(хоть человек с генами растения). Поведение организма не зашито в код, а **вычисляется из генов +формулами** через слой признаков. Этот документ описывает модель и порядок реализации (фазы G1–G5). +Дизайн потребителя-растений — в [растения.md](растения.md); гены заменяют его жёсткий `PlantGenome`. + +## Ключевые понятия + +- **`GeneDef`** — отдельный деф на ген, организм-агностичный: id, тип (числовой/дискретный/флаг), + диапазон/дефолт/разброс (для генерации особи), доминантность (дискретные — доминант/рецессив; + числовые — смешение), мутации (вероятность/величина), теги/категория и **эффекты** — вклады гена + в признаки через формулы. +- **Геном** — управляемый объект на сущности: набор генов **переменного состава** (id гена → пара + аллелей, диплоид). Поддерживает добавление/удаление/смешивание генов → гибриды. Скрещивание — + помейозно по объединению генов родителей + мутация. Сериализуется в сейв. +- **Признаки (traits)** — фенотип, вычисляемый из генома + среды: `growthSpeed`, `fruitYield`, + продукт добычи, цвет, период плодоношения, жизнь, расселение… **Системы читают готовые признаки**, + а не гены напрямую. Поэтому одна система плодоношения работает и для дерева, и для человека с + «плодовым» геном; и так «учитываются гены во всех расчётах». +- **Движок формул** — свой (без внешних библиотек) вычислитель выражений в дефах: числа, операции, + функции (`min/max/clamp/lerp/pow/exp/…`), переменные (значения генов/среды/других признаков). + Парсинг → AST → компиляция в делегат; детерминизм; покрыт тестами. +- **Регэкспы в дефах** — три применения: выбор/группировка генов по шаблону имени в формулах + (напр. «сумма всех `leaf_*`»); правила/патчи контента (применять патч ко всем дефам по шаблону); + валидация строковых полей дефов при загрузке. + +## Модель + +``` +GeneDef (данные) ──┐ + ├─► Геном особи (id гена → аллели) ──► [формулы эффектов + среда] ──► Признаки ──► Системы +GeneDef.effects ───┘ (движок формул) (traits) +``` + +- Генерация особи: из базового набора генов вида (деф организма ссылается на `GeneDef` с базовыми + значениями/разбросом) создаются аллели; всё сидируется `Random` мира — детерминизм. +- Признаки кэшируются на особи; пересчитываются при изменении генома (статические признаки) и/или по + среде (динамические — напр. итоговая скорость роста зависит ещё от света/температуры в точке). +- Скрещивание: для каждого гена потомок берёт по аллелю от каждого родителя (если ген есть у обоих; + если только у одного — наследуется с шансом), затем мутация по `GeneDef`. + +## Фазы + +Каждая фаза — отдельным планом (план → утверждение → коммит), как остальные работы. + +### G1 — движок формул (Core) + +Самодостаточный keystone, нужен всему остальному. +- Свой вычислитель выражений: лексер → парсер → AST → компиляция в `Func`. + Поддержка: числа, `+ - * / %`, скобки, унарный минус, сравнения/тернарник (для условий вроде + «плодоносит ли»), функции (`min,max,clamp,lerp,pow,exp,floor,round,abs,…`), переменные по имени. +- Контекст переменных — провайдер `name → value` (значения генов, среды, признаков). +- Детерминизм, без аллокаций при вычислении (компиляция один раз). Тесты на парсинг/вычисление/ошибки. +- Локация: `engine/src/MrGameEng.Core` (или `MrGameEng.Content`, ближе к дефам) — уточнится в плане. + +### G2 — фундамент генов (движок + контент-слой) + +- `GeneDef` (тип дефа) и реестр генов; парсинг эффектов как формул (зависит от G1). +- Управляемый `Genome` (компонент-объект): набор генов с аллелями; генерация из набора вида; + скрещивание + мутация; (де)сериализация. +- Слой признаков: вычисление `traits` из генома и эффектов (формулы) + провайдера среды; кэш и + инвалидация. API `Trait(name)` для систем. +- Без растительной специфики — общая машинерия для любых организмов. + +### G3 — миграция растений на гены/признаки + +- Рост/размножение/жизненный цикл растений читают **признаки** (`growthSpeed`, оптимум/толерантность + света/температуры/почвы, жизнь, расселение, самоопыление, мутации) вместо полей фиксированного + `PlantGenome`. +- `plants.json`/`GeneDef`: набор генов вида описывается данными; убираем жёсткий `PlantGenome`/`Breed`. +- Поведение эквивалентно текущему (фазы A–D), но теперь полностью data-driven через гены/формулы. +- Сейв популяции хранит динамический геном. + +### G4 — генный контент растений + +Новые механики как признаки + небольшие системы: +- **Плодоношение:** признаки «плодоносит ли» (флаг), тип плода (дискретный ген → продукт-деф), число + плодов, период/сезон плодоношения; система создаёт плоды у зрелых растений. +- **Добыча:** что даёт при сборе (дерево/грибы/трава/…) — дискретный ген → продукт; количество — числом. +- **Цвета/морфология:** тинт и вариации спрайта из генов; периоды и скорость роста — уже признаки. + +### G5 — регэкспы и контент-инструменты + +- Группировка генов по шаблону имени в формулах/эффектах (часть G1/G2, выносится сюда как фича). +- **Контент-патчи:** применять патч-деф ко всем дефам, чьё имя подходит под регэксп (массовые правки + модами поверх Core). +- **Валидация:** проверка строковых полей дефов (id/ключи/форматы) по шаблонам при загрузке мода. + +### Позже — животные и люди, гибриды + +Та же машинерия генома/признаков переиспользуется для животных и людей; гибриды — добавлением +«чужих» генов в геном (напр. фотосинтез человеку «растительным» геном, дающим признак, который +читает существующая система). Никаких спец-веток в коде — только новые `GeneDef` и формулы. + +## Принципы + +- **Гены — источник истины; формулы — мост; признаки — то, что читают системы.** Системы не знают + о конкретных видах, только о признаках. +- **Data-driven:** новый ген/эффект/признак — это деф и формула, а не код. +- **Детерминизм:** генерация, скрещивание и мутации — на сидированном `Random`; формулы чисты. +- **Движок vs игра:** движок даёт формулы, `GeneDef`, геном, вычисление признаков, регэксп-инструменты; + игра задаёт конкретные гены/формулы/системы-потребители (плодоношение, добыча) данными и кодом. diff --git a/engine b/engine index 79d406a..10898b0 160000 --- a/engine +++ b/engine @@ -1 +1 @@ -Subproject commit 79d406a9f4687fdb47a059a6b1e0593eb33e3061 +Subproject commit 10898b08a065ffc564c00473069e02b0e5e06c2a diff --git a/src/LittleSim/LittleSim.csproj b/src/LittleSim/LittleSim.csproj index c969813..0ad0e36 100644 --- a/src/LittleSim/LittleSim.csproj +++ b/src/LittleSim/LittleSim.csproj @@ -6,6 +6,7 @@ + diff --git a/src/LittleSim/Program.cs b/src/LittleSim/Program.cs index 80779f4..597fc93 100644 --- a/src/LittleSim/Program.cs +++ b/src/LittleSim/Program.cs @@ -1,6 +1,7 @@ using LittleSim.Scenes; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Host; // Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне. using var host = new GameHost( diff --git a/src/LittleSim/Scenes/BootScene.cs b/src/LittleSim/Scenes/BootScene.cs index b0597d7..9aa9832 100644 --- a/src/LittleSim/Scenes/BootScene.cs +++ b/src/LittleSim/Scenes/BootScene.cs @@ -5,6 +5,7 @@ using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -73,6 +74,6 @@ public sealed class BootScene : Scene Context.Services.Add(content); Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory)); content.Languages.SetLanguage(_settings.Language); - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.6f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.6f)); } } diff --git a/src/LittleSim/Scenes/CreditsScene.cs b/src/LittleSim/Scenes/CreditsScene.cs index d6a6848..b4e63e5 100644 --- a/src/LittleSim/Scenes/CreditsScene.cs +++ b/src/LittleSim/Scenes/CreditsScene.cs @@ -2,6 +2,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; @@ -40,7 +41,7 @@ public sealed class CreditsScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/LoadGameScene.cs b/src/LittleSim/Scenes/LoadGameScene.cs index 9e502fd..be85763 100644 --- a/src/LittleSim/Scenes/LoadGameScene.cs +++ b/src/LittleSim/Scenes/LoadGameScene.cs @@ -3,6 +3,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -73,7 +74,7 @@ public sealed class LoadGameScene : Scene { Context.Scenes.Switch( new WorldScene(save.ToConfig(), save), - Transition.Fade(0.6f) + Transitions.Fade(0.6f) ); } }, @@ -104,7 +105,7 @@ public sealed class LoadGameScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/MainMenuScene.cs b/src/LittleSim/Scenes/MainMenuScene.cs index 5800983..64dbc57 100644 --- a/src/LittleSim/Scenes/MainMenuScene.cs +++ b/src/LittleSim/Scenes/MainMenuScene.cs @@ -2,6 +2,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.UI; namespace LittleSim.Scenes; @@ -37,7 +38,7 @@ public sealed class MainMenuScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(scene, Transition.Fade(0.4f)); + Context.Scenes.Switch(scene, Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/NewWorldScene.cs b/src/LittleSim/Scenes/NewWorldScene.cs index 70d53d8..a379f2a 100644 --- a/src/LittleSim/Scenes/NewWorldScene.cs +++ b/src/LittleSim/Scenes/NewWorldScene.cs @@ -6,6 +6,7 @@ using LittleSim.Content; using LittleSim.UI; using Microsoft.Xna.Framework.Input; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -153,14 +154,14 @@ public sealed class NewWorldScene : Scene Seed = seed, SmoothPasses = _smoothing, }; - Context.Scenes.Switch(new WorldScene(config), Transition.Fade(0.6f)); + Context.Scenes.Switch(new WorldScene(config), Transitions.Fade(0.6f)); } private void Back() { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/PauseMenu.cs b/src/LittleSim/Scenes/PauseMenu.cs index 0395c9d..6071706 100644 --- a/src/LittleSim/Scenes/PauseMenu.cs +++ b/src/LittleSim/Scenes/PauseMenu.cs @@ -5,6 +5,7 @@ using LittleSim.UI; using Microsoft.Xna.Framework; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using Myra.Graphics2D.Brushes; using Myra.Graphics2D.UI; diff --git a/src/LittleSim/Scenes/SettingsScene.cs b/src/LittleSim/Scenes/SettingsScene.cs index d03ad04..ce1e79f 100644 --- a/src/LittleSim/Scenes/SettingsScene.cs +++ b/src/LittleSim/Scenes/SettingsScene.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using MrGameEng.Audio; using MrGameEng.Core; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.UI; using Myra.Graphics2D.UI; @@ -59,7 +60,7 @@ public sealed class SettingsScene : Scene { if (!Context.Scenes.IsTransitioning) { - Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); + Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f)); } } } diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index 31dcfaf..84461fd 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -10,7 +10,9 @@ using Microsoft.Xna.Framework.Input; using MrGameEng.Assets; using MrGameEng.Core; using MrGameEng.DevConsole; +using MrGameEng.Formulas; using MrGameEng.Graphics; +using MrGameEng.Host; using MrGameEng.Input; using MrGameEng.Inspector; using MrGameEng.Lighting; @@ -73,7 +75,7 @@ public sealed class WorldScene : Scene var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); var content = Context.Services.Get(); var atlases = Context.Services.Get(); - var device = Context.GraphicsDevice; + var device = Context.GetGraphicsDevice(); var input = this.UseInput(); _speed = Context.Services.Get(); _speed.SetStep(0); // новый мир/загрузка стартуют на x1 @@ -435,7 +437,7 @@ public sealed class WorldScene : Scene if (!Context.Scenes.IsTransitioning) { _speed.Resume(); - Context.Scenes.Switch(scene, Transition.Fade(0.5f)); + Context.Scenes.Switch(scene, Transitions.Fade(0.5f)); } } @@ -456,10 +458,34 @@ public sealed class WorldScene : Scene c.WriteLine($"regenerating world, seed {seed}"); Context.Scenes.Switch( new WorldScene(_config with { Seed = seed }), - Transition.Fade(0.6f) + Transitions.Fade(0.6f) ); } ); + console.Register( + "formula", + "formula — compile and evaluate an expression (gene-formula engine demo)", + (c, args) => + { + if (args.Length == 0) + { + c.WriteLine( + "usage: formula e.g. formula clamp(lerp(0, 10, 0.5), 0, 8)" + ); + return; + } + + var expression = string.Join(' ', args); + try + { + c.WriteLine($"{expression} = {Formula.Compile(expression).Evaluate()}"); + } + catch (FormulaException e) + { + c.WriteLine($"error: {e.Message}"); + } + } + ); console.Register( "menu", "menu — return to the main menu",