From 11f45fcf74e7a1b99b0eb203ba82a50c8b7c7a95 Mon Sep 17 00:00:00 2001 From: 6d486f49 <76097bcc@gmail.com> Date: Thu, 11 Jun 2026 17:34:32 -0400 Subject: [PATCH] ... --- ET/Web/Layout/NavMenu.razor | 5 + ET/Web/Pages/Simulation.razor | 220 ++++++++++++++ ET/Web/Program.cs | 1 + ET/Web/Services/GameSimulationService.cs | 306 ++++++++++++++++++++ ET/Web/wwwroot/css/app.css | 125 ++++++++ ET/Web/wwwroot/docs/notes-index.json | 5 +- ET/Web/wwwroot/docs/notes/forest-regions.md | 2 +- ET/Web/wwwroot/docs/notes/region-types.md | 3 +- ET/Web/wwwroot/docs/notes/water-regions.md | 3 + docs/.obsidian/workspace.json | 5 +- docs/Mountain Regions.md | 3 + 11 files changed, 671 insertions(+), 7 deletions(-) create mode 100644 ET/Web/Pages/Simulation.razor create mode 100644 ET/Web/Services/GameSimulationService.cs create mode 100644 docs/Mountain Regions.md diff --git a/ET/Web/Layout/NavMenu.razor b/ET/Web/Layout/NavMenu.razor index 1fe0b75..421d6e3 100644 --- a/ET/Web/Layout/NavMenu.razor +++ b/ET/Web/Layout/NavMenu.razor @@ -28,6 +28,11 @@ Gear + @if (groupedNotes == null) { diff --git a/ET/Web/Pages/Simulation.razor b/ET/Web/Pages/Simulation.razor new file mode 100644 index 0000000..e59b69f --- /dev/null +++ b/ET/Web/Pages/Simulation.razor @@ -0,0 +1,220 @@ +@page "/simulation" +@inject GameSimulationService SimService +@implements IDisposable + +Ecology Simulation + +
+

Ecology Simulation

+
+
+ +@if (!SimService.Data.IsInitialized) +{ +
+

Simulate 20 turns of predator, prey, and flora ecology across the valley.

+ +
+} +else +{ +
+
+
+ + + Turn @SimService.Data.CurrentTurn / 20 + + +
+
+ + @CurrentEvent?.MeepleType Ecology + + + in @string.Join(", ", CurrentEvent?.RegionTypes ?? new()) + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + @{ + var currentRegions = SimService.GetCurrentState(); + var activatedRegionNames = new HashSet(); + if (CurrentEvent != null) + { + activatedRegionNames = currentRegions + .Where(r => CurrentEvent.RegionTypes.Contains(r.Terrain)) + .Select(r => r.Name) + .ToHashSet(); + } + } + + @foreach (var line in GetConnectionLines(currentRegions)) + { + + } + + @foreach (var region in currentRegions) + { + var isActivated = activatedRegionNames.Contains(region.Name); + var terrainColor = GetTerrainColor(region.Terrain); + var labelX = region.X + 24; + + + + + @region.Name + + + P:@region.PredatorMeeples R:@region.PreyMeeples F:@region.FloraMeeples + + + } + +
+ + @if (CurrentEvent != null && CurrentEvent.Details.Count > 0) + { +
+
Turn @CurrentEvent.TurnNumber Details
+
+ @foreach (var detail in CurrentEvent.Details) + { +
@detail
+ } +
+
+ } + +
+ @{ + var totals = SimService.GetCurrentState(); + } +
Predators: @totals.Sum(r => r.PredatorMeeples)
+
Prey: @totals.Sum(r => r.PreyMeeples)
+
Flora: @totals.Sum(r => r.FloraMeeples)
+
+
+} + +@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 + { + "Predator" => "danger", + "Prey" => "warning", + "Flora" => "success", + _ => "secondary" + }; + + protected override void OnInitialized() + { + SimService.RunSimulation(); + } + + private void StartSimulation() + { + SimService.RunSimulation(); + } + + private void PrevTurn() + { + if (SimService.Data.CurrentTurn > 1) + SimService.Data.CurrentTurn--; + } + + private void NextTurn() + { + if (SimService.Data.CurrentTurn < 20) + SimService.Data.CurrentTurn++; + } + + private void RandomizeEvents() + { + SimService.RandomizeEvents(); + } + + private static string GetTerrainColor(string terrain) + { + return terrain switch + { + "Grass" => "#4caf50", + "Forest" => "#2e7d32", + "Mountain" => "#78909c", + "Water" => "#42a5f5", + "Wasteland" => "#8d6e63", + _ => "#888" + }; + } + + private record ConnectionLine(int X1, int Y1, int X2, int Y2); + + private List GetConnectionLines(List regions) + { + var lookup = regions.ToDictionary(r => r.Name); + var lines = new List(); + var drawn = new HashSet(); + + foreach (var region in regions) + { + foreach (var conn in region.Connections) + { + var key = string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) < 0 + ? $"{region.Name}|{conn}" + : $"{conn}|{region.Name}"; + + if (!drawn.Add(key)) continue; + if (!lookup.TryGetValue(conn, out var target)) continue; + + lines.Add(new ConnectionLine(region.X, region.Y, target.X, target.Y)); + } + } + + return lines; + } + + public void Dispose() + { + } +} diff --git a/ET/Web/Program.cs b/ET/Web/Program.cs index 67da680..71cffad 100644 --- a/ET/Web/Program.cs +++ b/ET/Web/Program.cs @@ -9,6 +9,7 @@ builder.RootComponents.Add("head::after"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(); +builder.Services.AddSingleton(); //builder.Services.AddTelerikBlazor(); diff --git a/ET/Web/Services/GameSimulationService.cs b/ET/Web/Services/GameSimulationService.cs new file mode 100644 index 0000000..2dd3473 --- /dev/null +++ b/ET/Web/Services/GameSimulationService.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Web.Services; + +public class RegionState +{ + public string Name { get; set; } = ""; + public string Slug { get; set; } = ""; + public string Terrain { get; set; } = ""; + public int X { get; set; } + public int Y { get; set; } + public List Connections { get; set; } = new(); + public int PredatorMeeples { get; set; } + public int PreyMeeples { get; set; } + public int FloraMeeples { get; set; } + + public RegionState Clone() + { + return new RegionState + { + Name = Name, + Slug = Slug, + Terrain = Terrain, + X = X, + Y = Y, + Connections = new List(Connections), + PredatorMeeples = PredatorMeeples, + PreyMeeples = PreyMeeples, + FloraMeeples = FloraMeeples + }; + } +} + +public class TurnEvent +{ + public int TurnNumber { get; set; } + public string MeepleType { get; set; } = ""; + public List RegionTypes { get; set; } = new(); + public List Details { get; set; } = new(); +} + +public class SimulationData +{ + public List Regions { get; set; } = new(); + public List InitialRegions { get; set; } = new(); + public List Events { get; set; } = new(); + public List> Timeline { get; set; } = new(); + public int CurrentTurn { get; set; } + public bool IsInitialized { get; set; } +} + +public class GameSimulationService +{ + private readonly Random _rng = new(); + + public SimulationData Data { get; private set; } = new(); + + public void RunSimulation() + { + Data = new SimulationData(); + var regions = CreateRegions(); + Data.Regions = regions; + AddInitialFlora(); + AddRandomMeepleCombos(); + Data.InitialRegions = Data.Regions.Select(r => r.Clone()).ToList(); + GenerateAndApplyEvents(); + Data.IsInitialized = true; + } + + public void RandomizeEvents() + { + var initialRegions = Data.InitialRegions; + Data = new SimulationData(); + Data.Regions = initialRegions.Select(r => r.Clone()).ToList(); + Data.InitialRegions = initialRegions; + GenerateAndApplyEvents(); + Data.IsInitialized = true; + } + + public List GetCurrentState() + { + return Data.Timeline[Data.CurrentTurn].Select(r => r.Clone()).ToList(); + } + + public TurnEvent? GetCurrentEvent() + { + return Data.Events.ElementAtOrDefault(Data.CurrentTurn - 1); + } + + private void GenerateAndApplyEvents() + { + Data.Timeline.Add(Data.Regions.Select(r => r.Clone()).ToList()); + + for (int turn = 1; turn <= 20; turn++) + { + var turnEvent = GenerateTurnEvent(turn); + ApplyTurnEvent(turnEvent); + Data.Events.Add(turnEvent); + Data.Timeline.Add(Data.Regions.Select(r => r.Clone()).ToList()); + } + + Data.CurrentTurn = 1; + } + + 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(); + + return new TurnEvent + { + TurnNumber = turnNumber, + MeepleType = meepleType, + RegionTypes = selectedTypes, + Details = new List() + }; + } + + private void ApplyTurnEvent(TurnEvent turnEvent) + { + var details = new List(); + + foreach (var region in Data.Regions) + { + if (!turnEvent.RegionTypes.Contains(region.Terrain)) + continue; + + switch (turnEvent.MeepleType) + { + case "Flora": + region.FloraMeeples++; + details.Add($"{region.Name}: +1 Flora"); + break; + + case "Predator": + ApplyPredatorActivation(region, details); + break; + + case "Prey": + ApplyPreyActivation(region, details); + break; + } + } + + turnEvent.Details = details; + } + + private void ApplyPredatorActivation(RegionState region, List details) + { + if (region.PredatorMeeples <= 0) + return; + + if (region.PreyMeeples > 0) + { + int eaten = Math.Min(region.PredatorMeeples, region.PreyMeeples); + region.PreyMeeples -= eaten; + region.PredatorMeeples += eaten; + details.Add($"{region.Name}: Predators ate {eaten} prey, now {region.PredatorMeeples}P / {region.PreyMeeples}R"); + } + else + { + var target = FindBestTravelTarget(region, r => r.PreyMeeples > 0); + if (target != null) + { + int traveling = region.PredatorMeeples; + target.PredatorMeeples += traveling; + region.PredatorMeeples = 0; + details.Add($"{region.Name}: {traveling} predators traveled to {target.Name}"); + } + } + } + + private void ApplyPreyActivation(RegionState region, List details) + { + if (region.PreyMeeples <= 0) + return; + + if (region.FloraMeeples > 0) + { + int consumed = Math.Min(region.PreyMeeples, region.FloraMeeples); + region.FloraMeeples -= consumed; + region.PreyMeeples += consumed; + details.Add($"{region.Name}: Prey consumed {consumed} flora, now {region.PreyMeeples}R / {region.FloraMeeples}F"); + } + else + { + var target = FindBestTravelTarget(region, r => r.FloraMeeples > 0); + if (target != null) + { + int traveling = region.PreyMeeples; + target.PreyMeeples += traveling; + region.PreyMeeples = 0; + details.Add($"{region.Name}: {traveling} prey traveled to {target.Name}"); + } + } + } + + private RegionState? FindBestTravelTarget(RegionState region, Func hasResource) + { + var direct = region.Connections + .Select(name => Data.Regions.FirstOrDefault(r => r.Name == name)) + .OfType() + .Where(r => hasResource(r)) + .ToList(); + + if (direct.Count > 0) + { + return direct.OrderByDescending(r => r.PreyMeeples + r.FloraMeeples).First(); + } + + var visited = new HashSet { region.Name }; + var queue = new Queue(region.Connections); + while (queue.Count > 0) + { + var currentName = queue.Dequeue(); + if (!visited.Add(currentName)) continue; + var current = Data.Regions.FirstOrDefault(r => r.Name == currentName); + if (current == null) continue; + if (hasResource(current)) + return current; + foreach (var conn in current.Connections) + { + if (!visited.Contains(conn)) + queue.Enqueue(conn); + } + } + + return null; + } + + private void AddInitialFlora() + { + var excluded = new HashSet + { + "Grass 1", "Grass 2", "Wasteland 1", + "Mountain 1", "Mountain 2", "Mountain 3", "Mountain 4", "Mountain 5" + }; + + foreach (var region in Data.Regions) + { + if (!excluded.Contains(region.Name)) + { + region.FloraMeeples = 1; + } + } + } + + private void AddRandomMeepleCombos() + { + var combos = new List<(int pred, int prey, int flora)>(); + for (int total = 1; total <= 3; total++) + { + for (int p = 0; p <= total; p++) + { + for (int r = 0; r <= total - p; r++) + { + int f = total - p - r; + combos.Add((p, r, f)); + } + } + } + + var shuffled = combos.OrderBy(_ => _rng.Next()).ToList(); + for (int i = 0; i < Data.Regions.Count; i++) + { + var (p, r, f) = shuffled[i % shuffled.Count]; + Data.Regions[i].PredatorMeeples = p; + Data.Regions[i].PreyMeeples = r; + Data.Regions[i].FloraMeeples = f; + } + } + + private static List CreateRegions() + { + return new List + { + new() { Name = "Grass 1", Slug = "grass-1", Terrain = "Grass", X = 15, Y = 260, Connections = new() { "Mountain 1", "Water 2", "Forest 1" } }, + new() { Name = "Grass 2", Slug = "grass-2", Terrain = "Grass", X = 150, Y = 400, Connections = new() { "Forest 1", "Forest 2" } }, + new() { Name = "Grass 3", Slug = "grass-3", Terrain = "Grass", X = 300, Y = 230, Connections = new() { "Water 2", "Mountain 2", "Mountain 3" } }, + new() { Name = "Grass 4", Slug = "grass-4", Terrain = "Grass", X = 510, Y = 30, Connections = new() { "Mountain 4", "Wasteland 1" } }, + new() { Name = "Grass 5", Slug = "grass-5", Terrain = "Grass", X = 550, Y = 290, Connections = new() { "Mountain 5", "Water 5", "Wasteland 1", "Forest 4" } }, + new() { Name = "Forest 1", Slug = "forest-1", Terrain = "Forest", X = 60, Y = 330, Connections = new() { "Grass 1", "Grass 2", "Water 1", "Forest 2" } }, + new() { Name = "Forest 2", Slug = "forest-2", Terrain = "Forest", X = 250, Y = 370, Connections = new() { "Grass 2", "Water 2", "Forest 1" } }, + new() { Name = "Forest 3", Slug = "forest-3", Terrain = "Forest", X = 150, Y = 125, Connections = new() { "Water 2", "Water 3" } }, + new() { Name = "Forest 4", Slug = "forest-4", Terrain = "Forest", X = 420, Y = 100, Connections = new() { "Grass 4", "Wasteland 1", "Grass 5" } }, + new() { Name = "Forest 5", Slug = "forest-5", Terrain = "Forest", X = 470, Y = 410, Connections = new() { "Water 5" } }, + new() { Name = "Mountain 1", Slug = "mountain-1", Terrain = "Mountain", X = 20, Y = 120, Connections = new() { "Grass 1", "Water 3", "Forest 3" } }, + new() { Name = "Mountain 2", Slug = "mountain-2", Terrain = "Mountain", X = 260, Y = 110, Connections = new() { "Water 3", "Forest 3" } }, + new() { Name = "Mountain 3", Slug = "mountain-3", Terrain = "Mountain", X = 380, Y = 180, Connections = new() { "Mountain 5", "Forest 4", "Grass 3" } }, + new() { Name = "Mountain 4", Slug = "mountain-4", Terrain = "Mountain", X = 370, Y = 30, Connections = new() { "Forest 4", "Grass 4" } }, + new() { Name = "Mountain 5", Slug = "mountain-5", Terrain = "Mountain", X = 430, Y = 330, Connections = new() { "Grass 5", "Mountain 3" } }, + new() { Name = "Water 1", Slug = "water-1", Terrain = "Water", X = 30, Y = 410, Connections = new() { "Forest 1" } }, + new() { Name = "Water 2", Slug = "water-2", Terrain = "Water", X = 140, Y = 260, Connections = new() { "Forest 1", "Forest 2", "Grass 1", "Forest 3", "Grass 3" } }, + new() { Name = "Water 3", Slug = "water-3", Terrain = "Water", X = 50, Y = 50, Connections = new() { "Mountain 1", "Mountain 2", "Forest 3" } }, + new() { Name = "Water 4", Slug = "water-4", Terrain = "Water", X = 640, Y = 110, Connections = new() { "Grass 4", "Wasteland 1" } }, + new() { Name = "Water 5", Slug = "water-5", Terrain = "Water", X = 600, Y = 400, Connections = new() { "Grass 5", "Forest 5" } }, + new() { Name = "Wasteland 1", Slug = "wasteland-1", Terrain = "Wasteland", X = 560, Y = 120, Connections = new() { "Forest 4", "Grass 4", "Grass 5", "Water 4" } } + }; + } +} diff --git a/ET/Web/wwwroot/css/app.css b/ET/Web/wwwroot/css/app.css index 8473261..d5b0289 100644 --- a/ET/Web/wwwroot/css/app.css +++ b/ET/Web/wwwroot/css/app.css @@ -196,6 +196,10 @@ code { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-file-text' viewBox='0 0 16 16'%3E%3Cpath d='M5 4a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm-.5 2.5A.5.5 0 0 1 5 6h6a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zM5 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1H5zm0 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1H5z'/%3E%3Cpath d='M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2zm10-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z'/%3E%3C/svg%3E"); } +.bi-graph-up-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-graph-up' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M0 0h1v15h15v1H0V0Zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07Z'/%3E%3C/svg%3E"); +} + .hero-section { background: linear-gradient(135deg, var(--bg-sidebar) 0%, var(--bg-dark) 100%); border: 1px solid var(--border-color); @@ -676,3 +680,124 @@ table.frontmatter td.fm-key { padding: 3rem !important; text-align: center !important; } + +/* Simulation Page */ +.simulation-layout { + max-width: 900px; + margin: 0 auto; +} + +.sim-controls { + background: var(--bg-sidebar); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 0.75rem 1.25rem; +} + +.turn-label { + font-size: 1.1rem; + color: var(--text-main); + min-width: 100px; + text-align: center; +} + +.btn-outline-light { + color: var(--text-main); + border-color: var(--border-color); +} + +.btn-outline-light:hover { + background: var(--primary); + border-color: var(--primary); + color: #fff; +} + +.btn-outline-light:disabled { + opacity: 0.3; +} + +.event-badge { + font-size: 0.85rem; + padding: 0.35em 0.75em; +} + +.sim-map-container { + border: 1px solid var(--border-color); + border-radius: 12px; + overflow: hidden; + background: #0f100d; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +.sim-svg { + width: 100%; + height: auto; + display: block; +} + +.sim-region { + transition: opacity 0.2s; +} + +.sim-active circle:last-child { + animation: pulse-glow 1.5s ease-in-out infinite; +} + +@keyframes pulse-glow { + 0%, 100% { stroke-opacity: 1; } + 50% { stroke-opacity: 0.4; } +} + +.sim-details { + background: var(--bg-sidebar); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1rem 1.25rem; + max-height: 180px; +} + +.sim-details-scroll { + display: flex; + flex-direction: column; + gap: 0.25rem; + overflow-y: auto; + max-height: 120px; +} + +.sim-detail-item { + font-size: 0.85rem; + color: var(--text-muted); + padding: 0.2rem 0; + border-bottom: 1px solid rgba(255,255,255,0.04); +} + +.sim-detail-item:last-child { + border-bottom: none; +} + +.sim-totals { + font-size: 0.9rem; + color: var(--text-main); +} + +.total-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; +} + +.bg-danger { + background-color: #e76f51 !important; +} + +.bg-warning { + background-color: #e9c46a !important; + color: #000 !important; +} + +.bg-success { + background-color: #4caf50 !important; +} diff --git a/ET/Web/wwwroot/docs/notes-index.json b/ET/Web/wwwroot/docs/notes-index.json index 7218bb6..7df4beb 100644 --- a/ET/Web/wwwroot/docs/notes-index.json +++ b/ET/Web/wwwroot/docs/notes-index.json @@ -160,7 +160,7 @@ { "slug": "forest-regions", "title": "Forest Regions", - "category": "RegionType" + "category": "Region Type" }, { "slug": "gauzeblade", @@ -485,7 +485,8 @@ }, { "slug": "water-regions", - "title": "Water Regions" + "title": "Water Regions", + "category": "Region Type" }, { "slug": "weather", diff --git a/ET/Web/wwwroot/docs/notes/forest-regions.md b/ET/Web/wwwroot/docs/notes/forest-regions.md index 560c531..a831480 100644 --- a/ET/Web/wwwroot/docs/notes/forest-regions.md +++ b/ET/Web/wwwroot/docs/notes/forest-regions.md @@ -1,3 +1,3 @@ --- -category: RegionType +category: Region Type --- diff --git a/ET/Web/wwwroot/docs/notes/region-types.md b/ET/Web/wwwroot/docs/notes/region-types.md index b6a452c..117b148 100644 --- a/ET/Web/wwwroot/docs/notes/region-types.md +++ b/ET/Web/wwwroot/docs/notes/region-types.md @@ -1,3 +1,4 @@ [[Forest Regions]] [[Water Regions]] -[[Grass Regions]] \ No newline at end of file +[[Grass Regions]] +[[Mountain Regions]] diff --git a/ET/Web/wwwroot/docs/notes/water-regions.md b/ET/Web/wwwroot/docs/notes/water-regions.md index e69de29..a831480 100644 --- a/ET/Web/wwwroot/docs/notes/water-regions.md +++ b/ET/Web/wwwroot/docs/notes/water-regions.md @@ -0,0 +1,3 @@ +--- +category: Region Type +--- diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json index a337629..87238c2 100644 --- a/docs/.obsidian/workspace.json +++ b/docs/.obsidian/workspace.json @@ -74,8 +74,7 @@ "title": "Bookmarks" } } - ], - "currentTab": 1 + ] } ], "direction": "horizontal", @@ -184,7 +183,7 @@ "bases:Create new base": false } }, - "active": "a4348c23136fecb0", + "active": "e8ba8e9287dfab25", "lastOpenFiles": [ "Tasks/Simulate the game state.md", "Notes/Terrain Deck.md", diff --git a/docs/Mountain Regions.md b/docs/Mountain Regions.md new file mode 100644 index 0000000..a831480 --- /dev/null +++ b/docs/Mountain Regions.md @@ -0,0 +1,3 @@ +--- +category: Region Type +---