From 61bfb188f67592d7bda97ad30c3809421156892c Mon Sep 17 00:00:00 2001 From: 6d486f49 <76097bcc@gmail.com> Date: Fri, 12 Jun 2026 14:55:34 -0400 Subject: [PATCH] ... --- ET/Web/Pages/Simulation.razor | 107 ++++++++++++++++++++--- ET/Web/Services/GameSimulationService.cs | 71 ++++++++++----- ET/Web/wwwroot/css/app.css | 19 ++++ 3 files changed, 162 insertions(+), 35 deletions(-) diff --git a/ET/Web/Pages/Simulation.razor b/ET/Web/Pages/Simulation.razor index 37d771b..8021e2a 100644 --- a/ET/Web/Pages/Simulation.razor +++ b/ET/Web/Pages/Simulation.razor @@ -20,23 +20,33 @@ else
- - Turn @SimService.Data.CurrentTurn / 20 + @if (SimService.Data.CurrentTurn == 0) + { + Initial Setup + } + else + { + After Turn @SimService.Data.CurrentTurn / 20 + }
-
- - @CurrentEvent?.MeepleType Ecology - - - in @string.Join(", ", CurrentEvent?.RegionTypes ?? new()) - +
+ @if (CurrentEvent != null) + { + @foreach (var act in CurrentEvent.Activations) + { + + @act.MeepleType in @act.RegionType + + } + }
@@ -70,11 +80,14 @@ else @{ var currentRegions = SimService.GetCurrentState(); + var prevRegions = SimService.GetPreviousState(); + var prevLookup = prevRegions.ToDictionary(r => r.Name); var activatedRegionNames = new HashSet(); if (CurrentEvent != null) { + var activatedTerrain = CurrentEvent.Activations.Select(a => a.RegionType).ToHashSet(); activatedRegionNames = currentRegions - .Where(r => CurrentEvent.RegionTypes.Contains(r.Terrain)) + .Where(r => activatedTerrain.Contains(r.Terrain)) .Select(r => r.Name) .ToHashSet(); } @@ -91,6 +104,11 @@ else var isActivated = activatedRegionNames.Contains(region.Name); var terrainColor = GetTerrainColor(region.Terrain); var labelX = region.X + 24; + var prev = prevLookup.GetValueOrDefault(region.Name); + var dp = prev != null ? region.PredatorMeeples - prev.PredatorMeeples : 0; + var dr = prev != null ? region.PreyMeeples - prev.PreyMeeples : 0; + var df = prev != null ? region.FloraMeeples - prev.FloraMeeples : 0; + var hasDelta = dp != 0 || dr != 0 || df != 0; P:@region.PredatorMeeples R:@region.PreyMeeples F:@region.FloraMeeples + @if (hasDelta && SimService.Data.CurrentTurn > 0) + { + var dy = region.Y + 34; + var parts = new List(); + if (dp != 0) parts.Add($" 0 ? "#4caf50" : "#e76f51")}\">{(dp > 0 ? "+" : "")}{dp}P"); + if (dr != 0) parts.Add($" 0 ? "#4caf50" : "#e76f51")}\">{(dr > 0 ? "+" : "")}{dr}R"); + if (df != 0) parts.Add($" 0 ? "#4caf50" : "#e76f51")}\">{(df > 0 ? "+" : "")}{df}F"); + var deltaHtml = $"{string.Join(" ", parts)}"; + @((MarkupString)deltaHtml) + } } @@ -131,15 +159,70 @@ else
Prey: @totals.Sum(r => r.PreyMeeples)
Flora: @totals.Sum(r => r.FloraMeeples)
+ +
+
Population Trend
+ + @{ + var history = SimService.GetPopulationHistory(); + var maxPop = Math.Max(history.Max(h => h.predators + h.prey + h.flora), 10); + var chartW = 700d; + var chartH = 120d; + var chartY = 20d; + var stepX = history.Length > 1 ? chartW / (history.Length - 1) : chartW; + + string MakePath(Func getVal, string color) + { + var pts = history.Select((h, i) => + { + var x = i * stepX; + var y = chartY + chartH - (getVal(i) / (double)maxPop) * chartH; + return $"{x:F1},{y:F1}"; + }); + var d = "M" + string.Join(" L", pts); + return $""; + } + + // Y axis labels + @((MarkupString)$"{maxPop}") + @((MarkupString)$"0") + + // Grid lines + + + + // Current turn marker + var markerX = SimService.Data.CurrentTurn * stepX; + + + @((MarkupString)MakePath(i => history[i].predators, "#e76f51")) + @((MarkupString)MakePath(i => history[i].prey, "#e9c46a")) + @((MarkupString)MakePath(i => history[i].flora, "#4caf50")) + + // Turn markers on x-axis + @for (int i = 0; i < history.Length; i += 5) + { + var x = i * stepX; + @((MarkupString)$"{i}") + } + } + +
+ Predators + Prey + Flora +
+
} + @code { private TurnEvent? CurrentEvent => SimService.Data.CurrentTurn > 0 && SimService.Data.CurrentTurn <= SimService.Data.Events.Count ? SimService.Data.Events[SimService.Data.CurrentTurn - 1] : null; - private string EventBadgeClass => CurrentEvent?.MeepleType switch + private static string BadgeClass(string meepleType) => meepleType switch { "Predator" => "danger", "Prey" => "warning", @@ -159,7 +242,7 @@ else private void PrevTurn() { - if (SimService.Data.CurrentTurn > 1) + if (SimService.Data.CurrentTurn > 0) SimService.Data.CurrentTurn--; } diff --git a/ET/Web/Services/GameSimulationService.cs b/ET/Web/Services/GameSimulationService.cs index dc3b473..ecaf2f9 100644 --- a/ET/Web/Services/GameSimulationService.cs +++ b/ET/Web/Services/GameSimulationService.cs @@ -33,11 +33,16 @@ public class RegionState } } +public class TerrainActivation +{ + public string MeepleType { get; set; } = ""; + public string RegionType { get; set; } = ""; +} + public class TurnEvent { public int TurnNumber { get; set; } - public string MeepleType { get; set; } = ""; - public List RegionTypes { get; set; } = new(); + public List Activations { get; set; } = new(); public List Details { get; set; } = new(); } @@ -86,7 +91,24 @@ public class GameSimulationService public TurnEvent? GetCurrentEvent() { - return Data.Events.ElementAtOrDefault(Data.CurrentTurn - 1); + return Data.CurrentTurn > 0 + ? Data.Events.ElementAtOrDefault(Data.CurrentTurn - 1) + : null; + } + + public List GetPreviousState() + { + var prev = Data.CurrentTurn > 0 ? Data.CurrentTurn - 1 : 0; + return Data.Timeline[prev].Select(r => r.Clone()).ToList(); + } + + public (int predators, int prey, int flora)[] GetPopulationHistory() + { + return Data.Timeline.Select(t => ( + t.Sum(r => r.PredatorMeeples), + t.Sum(r => r.PreyMeeples), + t.Sum(r => r.FloraMeeples) + )).ToArray(); } private void GenerateAndApplyEvents() @@ -101,23 +123,26 @@ public class GameSimulationService Data.Timeline.Add(Data.Regions.Select(r => r.Clone()).ToList()); } - Data.CurrentTurn = 1; + Data.CurrentTurn = 0; } private TurnEvent GenerateTurnEvent(int turnNumber) { string[] meepleTypes = { "Predator", "Prey", "Flora" }; - var meepleType = meepleTypes[_rng.Next(3)]; - string[] regionTypes = { "Grass", "Forest", "Mountain", "Water", "Wasteland" }; var numTypes = _rng.Next(1, 4); var selectedTypes = regionTypes.OrderBy(_ => _rng.Next()).Take(numTypes).ToList(); + var activations = selectedTypes.Select(rt => new TerrainActivation + { + RegionType = rt, + MeepleType = meepleTypes[_rng.Next(3)] + }).ToList(); + return new TurnEvent { TurnNumber = turnNumber, - MeepleType = meepleType, - RegionTypes = selectedTypes, + Activations = activations, Details = new List() }; } @@ -126,25 +151,25 @@ public class GameSimulationService { var details = new List(); - foreach (var region in Data.Regions) + foreach (var activation in turnEvent.Activations) { - if (!turnEvent.RegionTypes.Contains(region.Terrain)) - continue; - - switch (turnEvent.MeepleType) + foreach (var region in Data.Regions.Where(r => r.Terrain == activation.RegionType)) { - case "Flora": - region.FloraMeeples++; - details.Add($"{region.Name}: +1 Flora"); - break; + switch (activation.MeepleType) + { + case "Flora": + region.FloraMeeples++; + details.Add($"{region.Name}: +1 Flora"); + break; - case "Predator": - ApplyPredatorActivation(region, details); - break; + case "Predator": + ApplyPredatorActivation(region, details); + break; - case "Prey": - ApplyPreyActivation(region, details); - break; + case "Prey": + ApplyPreyActivation(region, details); + break; + } } } diff --git a/ET/Web/wwwroot/css/app.css b/ET/Web/wwwroot/css/app.css index d5b0289..4564388 100644 --- a/ET/Web/wwwroot/css/app.css +++ b/ET/Web/wwwroot/css/app.css @@ -801,3 +801,22 @@ table.frontmatter td.fm-key { .bg-success { background-color: #4caf50 !important; } + +/* Simulation Chart */ +.sim-chart { + background: var(--bg-sidebar); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1rem 1.25rem; +} + +.sim-chart-svg { + width: 100%; + height: auto; + display: block; +} + +.sim-chart-legend { + font-size: 0.85rem; + color: var(--text-muted); +}