G1: bump engine (formula engine + Host split), wire formula demo

Engine pointer -> 10898b0, which brings in two engine changes:
- the gene-system formula engine (Core): a data-driven expression
  evaluator that compiles a def string once and evaluates it
  allocation-free against a variable context;
- a parallel refactor splitting the platform out of Core into a new
  MrGameEng.Host library.

Game migration for the Host split: reference MrGameEng.Host (+ add it
and its test project to the solution), import MrGameEng.Host where
GameHost/GameHostOptions/Input are used, switch Transition.Fade ->
Transitions.Fade, and read the device via context.GetGraphicsDevice()
now that EngineContext is platform-free.

Showcase the formula engine with a dev-console 'formula <expr>' command
that compiles and evaluates an expression. Also adds docs/гены.md, the
gene-system design doc (phases G1-G5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 23:30:44 +03:00
co-authored by Claude Opus 4.8
parent 49936a1de3
commit 32bae03869
13 changed files with 181 additions and 12 deletions
+30
View File
@@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI.Tests", "engin
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio.Tests", "engine\tests\MrGameEng.Audio.Tests\MrGameEng.Audio.Tests.csproj", "{E0E37D87-4F62-41E9-9CD1-3E7432301508}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{E0E37D87-4F62-41E9-9CD1-3E7432301508}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -253,5 +281,7 @@ Global
{86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5} {86ECA4A0-46A2-45F4-8CF4-8FB8BB5DC050} = {CD3B5CE5-C23F-38B4-04AA-A303F94731A5}
{BC64F7AF-0FC3-4608-A4FF-1273AB8F2ABD} = {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} {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 EndGlobalSection
EndGlobal EndGlobal
+104
View File
@@ -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<context, float>`.
Поддержка: числа, `+ - * / %`, скобки, унарный минус, сравнения/тернарник (для условий вроде
«плодоносит ли»), функции (`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`, геном, вычисление признаков, регэксп-инструменты;
игра задаёт конкретные гены/формулы/системы-потребители (плодоношение, добыча) данными и кодом.
+1 -1
Submodule engine updated: 79d406a9f4...10898b08a0
+1
View File
@@ -6,6 +6,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Core\MrGameEng.Core.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Host\MrGameEng.Host.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" /> <ProjectReference Include="..\..\engine\src\MrGameEng.Content\MrGameEng.Content.csproj" />
+1
View File
@@ -1,6 +1,7 @@
using LittleSim.Scenes; using LittleSim.Scenes;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
// Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне. // Контент Core-мода грузится уже в окне — на загрузочном экране (BootScene), в фоне.
using var host = new GameHost( using var host = new GameHost(
+2 -1
View File
@@ -5,6 +5,7 @@ using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
@@ -73,6 +74,6 @@ public sealed class BootScene : Scene
Context.Services.Add(content); Context.Services.Add(content);
Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory)); Context.Services.Add(new ModAtlases(content.AtlasCacheDirectory));
content.Languages.SetLanguage(_settings.Language); content.Languages.SetLanguage(_settings.Language);
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.6f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.6f));
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.UI; using MrGameEng.UI;
@@ -40,7 +41,7 @@ public sealed class CreditsScene : Scene
{ {
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
} }
+3 -2
View File
@@ -3,6 +3,7 @@ using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
@@ -73,7 +74,7 @@ public sealed class LoadGameScene : Scene
{ {
Context.Scenes.Switch( Context.Scenes.Switch(
new WorldScene(save.ToConfig(), save), 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) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
} }
+2 -1
View File
@@ -2,6 +2,7 @@ using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.UI; using MrGameEng.UI;
namespace LittleSim.Scenes; namespace LittleSim.Scenes;
@@ -37,7 +38,7 @@ public sealed class MainMenuScene : Scene
{ {
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch(scene, Transition.Fade(0.4f)); Context.Scenes.Switch(scene, Transitions.Fade(0.4f));
} }
} }
} }
+3 -2
View File
@@ -6,6 +6,7 @@ using LittleSim.Content;
using LittleSim.UI; using LittleSim.UI;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
@@ -153,14 +154,14 @@ public sealed class NewWorldScene : Scene
Seed = seed, Seed = seed,
SmoothPasses = _smoothing, 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() private void Back()
{ {
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
} }
+1
View File
@@ -5,6 +5,7 @@ using LittleSim.UI;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using Myra.Graphics2D.Brushes; using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
+2 -1
View File
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using MrGameEng.Audio; using MrGameEng.Audio;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.UI; using MrGameEng.UI;
using Myra.Graphics2D.UI; using Myra.Graphics2D.UI;
@@ -59,7 +60,7 @@ public sealed class SettingsScene : Scene
{ {
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
Context.Scenes.Switch(new MainMenuScene(), Transition.Fade(0.4f)); Context.Scenes.Switch(new MainMenuScene(), Transitions.Fade(0.4f));
} }
} }
} }
+29 -3
View File
@@ -10,7 +10,9 @@ using Microsoft.Xna.Framework.Input;
using MrGameEng.Assets; using MrGameEng.Assets;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Formulas;
using MrGameEng.Graphics; using MrGameEng.Graphics;
using MrGameEng.Host;
using MrGameEng.Input; using MrGameEng.Input;
using MrGameEng.Inspector; using MrGameEng.Inspector;
using MrGameEng.Lighting; using MrGameEng.Lighting;
@@ -73,7 +75,7 @@ public sealed class WorldScene : Scene
var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets(); var assets = Context.Services.GetOrDefault<AssetManager>() ?? Context.UseAssets();
var content = Context.Services.Get<GameContent>(); var content = Context.Services.Get<GameContent>();
var atlases = Context.Services.Get<ModAtlases>(); var atlases = Context.Services.Get<ModAtlases>();
var device = Context.GraphicsDevice; var device = Context.GetGraphicsDevice();
var input = this.UseInput(); var input = this.UseInput();
_speed = Context.Services.Get<GameSpeed>(); _speed = Context.Services.Get<GameSpeed>();
_speed.SetStep(0); // новый мир/загрузка стартуют на x1 _speed.SetStep(0); // новый мир/загрузка стартуют на x1
@@ -435,7 +437,7 @@ public sealed class WorldScene : Scene
if (!Context.Scenes.IsTransitioning) if (!Context.Scenes.IsTransitioning)
{ {
_speed.Resume(); _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}"); c.WriteLine($"regenerating world, seed {seed}");
Context.Scenes.Switch( Context.Scenes.Switch(
new WorldScene(_config with { Seed = seed }), new WorldScene(_config with { Seed = seed }),
Transition.Fade(0.6f) Transitions.Fade(0.6f)
); );
} }
); );
console.Register(
"formula",
"formula <expr> — compile and evaluate an expression (gene-formula engine demo)",
(c, args) =>
{
if (args.Length == 0)
{
c.WriteLine(
"usage: formula <expr> 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( console.Register(
"menu", "menu",
"menu — return to the main menu", "menu — return to the main menu",